<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue响应式原理</title>
  </head>
  <body>
    <script>
      // f
      function render() {
        console.log('模拟视图渲染');
      }

      // 收集依赖
      function observe(obj) {
        // 判断类型
        if (!obj || typeof obj !== 'object') {
          return;
        }
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key])
        })
      }

      function defineReactive(obj, key, value) {
        // 递归子属性
        observe(value);

        // let dp = new Dep();
        Object.defineProperty(obj, key, {
          enumerable: true, // 可枚举（可以遍历）
          configurable: true, // 可以配置（比如可以删除）
          get: function reactiveGetter() {
            console.log('get', value); // 监听
            // 将 Watcher 添加到订阅
            if (Dep.target) {
              dp.addSub(Dep.target); // 新增
            }
            return value;
          },
          set: function reactiveSetter(newValue) {
            observe(newValue); // 如果赋值是一个对象，也要递归子属性
            if (newValue !== value) {
              console.log('set', newValue); //监听
              render();
              value = newValue;
              // 执行watcher 的 update 方法
              dp.notify();
            }
          }
        })
      }

      // 测试1
      let data = {
        name: 'David',
        location: { x: 100, y: 100}
      }
      observe(data)

      // 1. Object.defineProperty不能监听数组的变化，需要进行数组方法的重写
      let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push'];
      // 先获取原来的原型上的方法
      let arrayProto = Array.prototype;
      // 创建一个自己的原型，并重写methods里方法
      let proto = Object.create(arrayProto);
      methods.forEach(method => {
        proto[method] = function() {
          // AOP
          arrayProto[method].call(this, ...arguments);
          render();
        }
      })

      function observer(obj) {
        // 把所有的属性定义成set/get的方法
        if (Array.isArray(obj)) {
          obj.__proto__ = proto;
          return;
        }
        if (typeof obj === 'object') {
          for (let key in obj) {
            defineReactive(obj, key, obj[key]);
          }
        }
      }
      // 测试2
      let obj = [1, 2, 3];
      observer(obj);
      function $set(data, key, value) {
        defineReactive(data, key, value);
      }
      
      // 2. Proxy
      let handler = {
        get(target, key) {
          // 如果取的值是对象就在，就在这个对象进行数据劫持
          if (typeof target[key] == 'object' && target[key] !== '') {
            return new Proxy(target[key], handler);
          }
          return Reflect.get(target, key);
        },
        set(target, key, value) {
          if (key === 'length') return true;
          render();
          return Reflect.set(target, key, value);
        }
      }

      // 测试3
      let obj2 = {
        name:'Tom',
        age: {
          age: 100
        },
        arr:[1, 2, 3]
      }
      let proxy = new Proxy(obj2, handler);
      proxy.age.name = 'haha';    // 新增属性
      console.log(proxy.age.name) // 
      proxy.arr[0] = '1000';    //支持数组的内容变化监听
      console.log(proxy.arr);   //
      proxy.arr.length--;       //无效

      // 订阅者
      // 简单实现
      class Dep {
        constructor() {
          // 用来存放Watcher对象的数组
          this.subs = [];
        }
        // 在subs 中添加一个watcher对象
        addSub(sub) {
          this.subs.push(sub)
        }
        // 通知所有watcher对象更新视图
        notify() {
          this.subs.forEach(sub => {
            sub.update();
          })
        }
      }

      // 观察者
      // 简单实现
      class Watcher {
        constructor(obj, key, cb) {
          // 将Dep.target指向自己
          // 然后触发属性的getter添加监听
          // 最后将Dep.target置空
          Dep.target = this;
          this.cb = cb;
          this.obj = obj;
          this.key = key;
          this.value = obj[key];
          Dep.target = '';
        }

        update() {
          // 获取最新值
          this.value = this.obj[this.key];
          // 我们定义一个cb函数，这个函数用来模拟视图更新，调用它既可代表视图更新
          this.cb(this.value);
        }
      }

      class Vue{
        constructor(options) {
          this._data = options.data;
          observe(this._data);
          new Watcher();
          console.log('模拟视图渲染');
        }
      }

      // https://www.toutiao.com/a6713866890470490627/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1563247806&app=news_article_social&utm_source=weixin&utm_medium=toutiao_ios&req_id=201907161130060100170320133262F39&group_id=6713866890470490627
    </script>
  </body>
</html>
